//
//  BBNodeController.m
//  xNose
//
//  Created by ben smith on 9/7/08.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "BBNodeController.h"
#import "BBLibraryController.h"
#import "BBNode.h"
#import "BBRootNodeView.h"
#import "BBLibraryView.h"
#import "BBLeafNodeView.h"
#import "BBTabView.h"
#import "BBDimView.h"

#import "BBNodeViewFactory.h"
#import "BBOverlayView.h"
#import "BBGrandTotalView.h"
#import "BBUnderlayView.h"
#import "BBViewThumbnailer.h"
#import "BBZoomView.h"
#import "BBToolTabView.h"
#import "BBFileTabView.h"
#import "BBBlobDrawView.h"

@implementation BBNodeController

@synthesize rootNode;
@synthesize connectionSource;
@synthesize nodeSnapPoint;
@synthesize currentRootView;
@synthesize dimmingView;
@synthesize underlayView;
@synthesize scale;
@synthesize blobDrawView;

id sharedNodeController;

// our singleton
+(BBNodeController*)sharedNodeController
{
	return sharedNodeController;
	
}

-(void)awakeFromNib
{
	// setup all my various notifcations
	sharedNodeController = self;
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(releaseEdit:) name:@"BBReleaseEditNotification" object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(expandConnectedNodes:) name:@"BBDiscloseConnected" object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(zoomNotification:) name:@"BBZoomNotification" object:nil];
	
	[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"BBShowGrandTotals" options:NSKeyValueObservingOptionNew context:nil];
	isDraggingNode = NO;
	editingCount = 0;
	// this point is really just the starting x value for containers and the height of the current view
	nodeSnapPoint = NSMakePoint(0, 700);
	scale = 1.0;
	[self resetDefaultView];
	[self newTree];
}

-(NSRect)trashRect
{
	return NSMakeRect(NSMidX([currentRootView frame]) - 50, NSMinY([currentRootView frame]) + 40, 100, 100);	
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	if ([[NSUserDefaults standardUserDefaults] boolForKey:@"BBShowGrandTotals"]) {
		[currentRootView addSubview:grandTotalView];
	} else {
		[grandTotalView removeFromSuperview];
	}
}

// make a clean tree
// add a few nodes, otherwise things get wanky
-(void)newTree
{
	BBNode * root = [BBNode rootNode];
	self.rootNode = root;
	
	BBNode * container = [BBNode containerNode];	
	BBNode * collection = [BBNode collectionNode];
	BBNode * leaf1 = [BBNode leafNode];
	[container addChild:collection];
	[collection addChild:leaf1];
	[root addChild:container];
}


-(void)zoomNotification:(id)note
{
	// check the dimming view
	if (([dimmingView superview] == underlayView) && (![dimmingView canDrag])) return;
	
	isActive = YES;

	float newScale;
	NSPoint scaledOffset = NSMakePoint(nodeSnapPoint.x - NSMidX([currentRootView bounds]), nodeSnapPoint.y - NSMidY([currentRootView bounds]));
	
	if ([note isKindOfClass:[NSNotification class]]) {
		// first undo the previous scale
		newScale = [[note object] floatValue];
	}
	if ([note isKindOfClass:[NSNumber class]]) {
		// first undo the previous scale
		newScale = [note floatValue];
	}
	// find the ratio
	float ratio = newScale/self.scale;
	if (ratio == 1.0) return;
	self.scale = newScale;
	for (BBNode* kid in rootNode.children) {
		[kid setViewScale:ratio];
	}
	scaledOffset.x *= ratio;
	scaledOffset.y *= ratio;
	nodeSnapPoint.x = NSMidX([currentRootView bounds]) + scaledOffset.x;
	nodeSnapPoint.y = NSMidY([currentRootView bounds]) + scaledOffset.y;
	[self resetAllNodes:NO];
	if ([dimmingView superview] == underlayView) {
		[overlayView shuffleConnectedViews];
	}
	[self performSelector:@selector(clearActive) withObject:nil afterDelay:0.6];				
}

-(void)setActive:(BOOL)b
{
	if (!b) [self clearActive];
	isActive = b;
}
// used to report whether something is currently animating
-(BOOL)isActive
{
	return isActive;
}

// clears teh active flag.  this is normally called from a delayed message
-(void)clearActive
{
	isActive = NO;
	[overlayView setNeedsDisplay:YES];
}

// send the focus to our bogus focus-sucking textfield that sits behind all the views
// this is just to make it easy ti get rid of the focus ring 
-(void)releaseEdit:(id)sender
{
	[[bogus window] makeFirstResponder:bogus];
}

// goes through each container and makes sure that the various nodes are in the proper places
// if not, it animates their moving.  this is handy if one of the nodes has expanded, or a node
// has been removed or added in
-(void)resetAllNodes
{
	[self resetAllNodes:YES];
}

-(void)resetAllNodes:(BOOL)animate
{
	for (BBNode * container in rootNode.children) {
		BBContainerNodeView * cView = (BBContainerNodeView *)container.view;
		// reset this container
		[BBNodeViewFactory resetContainer:cView animate:animate];		
	}
	// clear the active flag after the animation is finished
	if (animate) {
		[self performSelector:@selector(clearActive) withObject:nil afterDelay:0.6];				
	} else {
		[self clearActive];
	}
	[self resetTopLayers];
}

// this moves a node in the tree next to another node also in the tree
-(void)moveNode:(BBNode*)aNode nextTo:(BBNode*)existingNode above:(BOOL)above
{
	if (aNode == existingNode) return; // dont do anythign if i am the same node
	
	// set the active flag so that the overlay will stop drawing during the animation
	isActive = YES;
	[overlayView setNeedsDisplay:YES];
	
	// remove the node from wher eit was
	[aNode.parent removeChild:aNode];
	
	// add it back in in the desired place
	BBNode * parent = existingNode.parent;
	
	NSInteger index = [[parent children] indexOfObject:existingNode];
	if (!above) index ++;
	[parent insertChild:aNode atIndex:index];
	
	// now reset all the frame positions so the animation is all cool
	[self resetAllNodes];
}

// just like move node, but with a new node instead of a node that is already in the tree
-(void)addNode:(BBNode*)aNode nextTo:(BBNode*)existingNode above:(BOOL)above
{
	// set the active flag so that the overlay will stop drawing during the animation
	isActive = YES;
	[overlayView setNeedsDisplay:YES];

	
	// just add me into the tree at the right spot
	BBNode * parent = existingNode.parent;
	
	NSInteger index = [[parent children] indexOfObject:existingNode];
	if (!above) index ++;
	[parent insertChild:aNode atIndex:index];
	
	// now i need to add some corresponding views for my new node data
	[BBNodeViewFactory loadNodes:aNode intoView:underlayView];

	//aNode is new, it needs to be scaled to the current scale
	[aNode setViewScale:self.scale];
	
	// now reset all the frame positions to take into account the new node
	[self resetAllNodes];

	// after I am done adding, check to see if i need to scroll to the visible rectangle
	[self performSelector:@selector(checkScrollPoint:) withObject:aNode afterDelay:0.5];
}

// this just checks to see if the supplied node falls in the visible
// rect, and if not it scrolls us to it
-(void)checkScrollPoint:(BBNode*)aNode
{
	isActive = YES;
	[overlayView setNeedsDisplay:YES];	
	if (aNode.position.x > (NSWidth([currentRootView frame]) - NSWidth([aNode.view frame]))) {
		float dist =(aNode.position.x - (NSWidth([currentRootView frame])/2.0)) * -1.0;
		[self scrollAllNodes:dist animate:YES];
	}	
	[self performSelector:@selector(clearActive) withObject:nil afterDelay:0.6];		
}

// destroys a node and removes all mention of it from any other node in the tree
-(void)killNode:(BBNode*)aNode
{
	// first kill the children
	NSArray * kids = [NSArray arrayWithArray:aNode.children];
	for (BBNode* kid in kids) {
		[self killNode:kid];
	}
	// now kill my own stuff
	[aNode clearConnections]; // clear all my connections
	[rootNode removeConnectedNode:aNode recurse:YES]; // clear any conenctions to me
	[aNode.parent removeChild:aNode]; // remove myself from the tree
}

// removes a node from the tree, permanently
-(void)removeNode:(BBNode*)aNode
{
	isActive = YES;
	[overlayView setNeedsDisplay:YES];

	[aNode removeFromView]; // first remove all the node views
	
	// need to obliterate this node and all connections to and from it
	[self killNode:aNode];

	// animate the other nodes
	[self resetAllNodes];
}

// this finds all the node collections that contain conected nodes and expands them
-(void)expandConnectedNodes:(BBNode*)aNode
{
	[rootNode openConnected];
	isActive = YES;
	[overlayView setNeedsDisplay:YES];
	// great, now reset all the containers
	[self resetAllNodes];
}

// expands a single node
-(void)expandNode:(BBNode*)aNode
{
	// expands the colleciton node
	if (![aNode isCollection]) return;
	// turn off other drawing during the animation
	isActive = YES;
	[overlayView setNeedsDisplay:YES];
	// kil lthe editor if it is in use
	[self releaseEdit:nil];
	
	// set the node to expand
	aNode.isOpen = YES;
	
	// ok, find the parent container
	// then find the container view
	// then reset the positions inside the container
	BBNode * container = aNode.parent;
	BBContainerNodeView * cView = (BBContainerNodeView *)container.view;
	[BBNodeViewFactory resetContainer:cView animate:YES];
	
	// turn other drawing back on after the animation
	[self performSelector:@selector(clearActive) withObject:nil afterDelay:0.5];
	[self resetTopLayers];
}

// just like expand node, but in reverse
-(void)collapseNode:(BBNode*)aNode
{
	// expands the colleciton node
	if (![aNode isCollection]) return;
	isActive = YES;
	[overlayView setNeedsDisplay:YES];
	[self releaseEdit:nil];
	aNode.isOpen = NO;
	// ok, find the parent container
	// then find the container view
	// then reset the positions inside the container
	BBNode * container = aNode.parent;
	BBContainerNodeView * cView = (BBContainerNodeView *)container.view;
	[BBNodeViewFactory resetContainer:cView animate:YES];
	[self performSelector:@selector(clearActive) withObject:nil afterDelay:0.5];
	[self resetTopLayers];
}


// like a hittest on a view, but we are only looking in specific places, not in the 
// entire view
-(NSView*)hitTest:(NSPoint)p
{		
	// first let us check the library
	NSView * libHit = [[BBLibraryController sharedLibraryController] hitTest:p];
	if (libHit != nil) return libHit;
	
	// now check to see if there is a node that is hit
	BBNode * hit = [rootNode hitTest:p];
	if (hit != nil) return hit.view;
	return nil;
}

// returns the 'hit test' but for the specified node type
// ie if the node is a collection but the hit test returns a 
// leaf, then it backs up to the parent and returns that instead
-(NSView*)hitTest:(NSPoint)p forNode:(BBNode*)aNode
{
	BBNode * hit = [rootNode hitTest:p];
	if (hit == nil) return nil;
	
	// certain node types can only 'hit' certain other types
	// for instance you dont want to be able to add a container to the tree next to a leaf
	
	if ([hit isSameTypeAs:aNode]) return hit.view;
	
	if ([aNode isLeaf]) {
		BBNode * firstNode = [hit firstLeaf];	
		return firstNode.view;
	}
	if ([aNode isCollection]) {
		BBNode * firstNode = [hit firstCollection];	
		return firstNode.view;
	}
	if ([aNode isContainer]) {
		BBNode * firstNode = [hit firstContainer];	
		return firstNode.view;
	}
	
	return nil;
}

// sets the nodes scroll point to a specified vale can be animated
-(void)setNodeScrollPoint:(float)xShift animate:(BOOL)animate
{
	float shiftBy = xShift - nodeSnapPoint.x;
	[self scrollAllNodes:shiftBy animate:animate];
}

// scrolls all the nodes by a relative amount
-(void)scrollAllNodes:(float)xShift animate:(BOOL)animate
{
	[self scrollAllNodesX:xShift y:0.0 animate:animate];
}

// scrolls all the nodes by a relative amount
-(void)scrollAllNodesX:(float)xShift y:(float)yShift animate:(BOOL)animate
{
	NSPoint p = self.nodeSnapPoint;
	p.x += xShift;
	p.y += yShift;
	self.nodeSnapPoint = p;
	
	//except the root node, naturally :-)
	for (BBNode * node in rootNode.children) {
		[node shiftViewFrameBy:NSMakePoint(xShift, yShift) animate:animate];
	}
	[overlayView setNeedsDisplay:YES];
}


//TODO: make the connection register it's own object
// this is how the connections get built
// any time you hit a connection butt, the leaf registers itself here
// if there is a waiting connection then they are joined
// if they were already joined, then they are severed
// if it is the same node, then clear all connections to and from that node
-(void)registerConnectionSource:(BBNode*)aNode
{
	// turn on the show connections whenever we start this process
	[[NSNotificationCenter defaultCenter] postNotificationName:@"BBShowConnected" object:self];

	// if there is a connection source waiting for and endpoint
	if (self.connectionSource != nil) {
		// then it is actually the end of the connection
		if (self.connectionSource == aNode) {
			// clear all the connections
			[self.connectionSource clearConnections];
			[rootNode removeConnectedNode:self.connectionSource recurse:YES];
		} else {
			// if we are already connected then we want to remove that conenction
			if ([self.connectionSource isConnectedTo:aNode]) {
				[self.connectionSource removeConnectedNode:aNode recurse:NO];
			} else {
				// can be connected in either direction, so check both
				if ([aNode isConnectedTo:self.connectionSource]) {
					[aNode removeConnectedNode:self.connectionSource recurse:NO];
				} else {
					// we made it! make the connection
					[self.connectionSource addConnection:aNode];									
				}
			}
		}
		// reset all my connection state variables
		self.connectionSource.isConnecting = NO;
		[self.connectionSource.view setNeedsDisplay:YES];
		self.connectionSource = nil;
		[self resetTopLayers];
		return;
	}
	// no source waiting, we must be the first one, set up some state variables
	self.connectionSource = aNode;
	aNode.isConnecting = YES;
	[aNode.view setNeedsDisplay:YES];
	[self resetTopLayers];
}

-(void)resetDefaultView
{
	self.currentRootView = rootView;
}



-(IBAction)refreshView:(id)sender
{
	// refresh the view hierarchy
	// do this the hard way for now
	// currently hardcoded to 1024x768
	
	
	// clear out my root view
	NSArray * subs = [NSArray arrayWithArray:[currentRootView subviews]];
	for (NSView * subview in subs) {
		[subview removeFromSuperview];
	}
	// load the node tree and all views into the supplied view
	[self loadViewInto:currentRootView];
}

// this is a pretty major method
// it builds the starting view and puts everything where it needs to be 
-(void)loadViewInto:(NSView*)aView
{
	nodeSnapPoint = NSMakePoint(0, NSHeight([aView frame]) - 80);

	self.currentRootView = aView;
	// first, save this view, we will need it in other methods
	
	// remove the entire tree from whatever view it had been in
	[self.rootNode removeFromView];
	
	// make my bogus text field
	bogus = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 150, 20)];
	[aView addSubview:bogus];
	
	// grab the bounds rect, i will need this for much of the layout
	NSRect bounds = [aView bounds];

	// now we build teh view structure from the ground up
	
	[underlayView release];
	// make a new underlayview it shoudl cover the whole bounds
	underlayView = [[BBUnderlayView alloc] initWithFrame:bounds];
	[aView addSubview:underlayView];
	
	// need a root view, it is the background, so it is also bounds size
	BBRootNodeView * root = [[BBRootNodeView alloc] initWithFrame:bounds];
	// add the root to the underlay
	[underlayView addSubview:root];
	[root setNode:self.rootNode];
	
	// now add all the node views 
	/// this will generate and position all the node views into the underlayview
	[BBNodeViewFactory loadNodes:self.rootNode intoView:underlayView];
	[self resetAllNodes];

	// now we have all the nodes accounted for, we want to add all the utility views
	float rightTabOffset = [[NSUserDefaults standardUserDefaults] floatForKey:@"BBRightTabOffset"];

	// the overlay view will draw all the connections and highlights, it also hosts the info and controller
	// views
	[overlayView removeFromSuperview];
	[overlayView release];
	overlayView = [[BBOverlayView alloc] initWithFrame:bounds];
	[underlayView addSubview:overlayView];
	
	/// add both the toolbars
//	NSRect toolTabFrame = NSMakeRect(NSMaxX(bounds) - rightTabOffset + 2,  NSMaxY(bounds) - 58, 300, 52);
//	[fileTabView removeFromSuperview];
//	[fileTabView release];
//	fileTabView = [[BBFileTabView alloc] initWithFrame:toolTabFrame];
//	[underlayView addSubview:fileTabView];
	
//	toolTabFrame.origin.y -= 55;
//	[toolTabView removeFromSuperview];
//	[toolTabView release];
//	toolTabView = [[BBToolTabView alloc] initWithFrame:toolTabFrame];
//	[underlayView addSubview:toolTabView];
	
	// now add the library view
	float libHeight = 696;
	NSRect libFrame = NSMakeRect(NSMaxX(bounds) - rightTabOffset + 2,  NSMaxY(bounds) - 20 - libHeight, 326, libHeight);
	
	[[[BBLibraryController sharedLibraryController] libView] removeFromSuperview];

	BBLibraryView * libview = [[BBLibraryView alloc] initWithFrame:libFrame];
	[underlayView addSubview:libview];

	[[BBLibraryController sharedLibraryController] setLibView:libview];
	

	// we dont add the dimming view just yet, but get it ready
	[dimmingView removeFromSuperview];
	[dimmingView release];
	dimmingView = [[BBDimView alloc] initWithFrame:bounds];
	[(BBDimView*)dimmingView setCanDrag:NO];

	// last, add the grand total view.
	// this actually goes on top of teh underlay view in the main view
	// it never moves
	NSRect gtFrame = NSMakeRect(NSMinX(bounds) + 30 ,  NSMinY(bounds), NSWidth(bounds) - 60, 80);
	grandTotalView = [[BBGrandTotalView alloc] initWithFrame:gtFrame];
	[(BBGrandTotalView*)grandTotalView setNode:rootNode];
	
	if ([[NSUserDefaults standardUserDefaults] boolForKey:@"BBShowGrandTotals"]) [aView addSubview:grandTotalView];

	[self resetTopLayers];
	
	// add the last layer
	[blobDrawView release];
	blobDrawView = [[BBBlobDrawView alloc] initWithFrame:bounds];
	[currentRootView addSubview:blobDrawView];
}

// effectively turns dimming on
// the dim layer will also block all mouse events,
// unless the drag flag is set
-(void)addDimLayer:(BOOL)allowsDrag
{
	[(BBDimView*)dimmingView setCanDrag:allowsDrag];
	[[underlayView animator] addSubview:dimmingView];
	[self resetTopLayers];
}

// this will reshuffle the top layers into the right positions
// they can get out of whack when new nodes are added and whatnot
-(void)resetTopLayers
{
	[[[BBLibraryController sharedLibraryController] libView] removeFromSuperview];
	[overlayView removeFromSuperview];
	//[fileTabView removeFromSuperview];	
	//[toolTabView removeFromSuperview];
	if ([dimmingView superview] == underlayView) {
		[dimmingView removeFromSuperview];
		[underlayView addSubview:dimmingView];
	}
	
	
	[underlayView addSubview:overlayView];
	//[underlayView addSubview:toolTabView];	
	//[underlayView addSubview:fileTabView];	
	[underlayView addSubview:[[BBLibraryController sharedLibraryController] libView]];

	
	[underlayView setNeedsDisplay:YES];
	[overlayView setNeedsDisplay:YES];
}

// turns the dimming 'off'
-(void)removeDimLayer
{
	[[dimmingView animator] removeFromSuperview];
}

// the ugly hardcoded default node tree. mostly this is just for testing
-(void)makeDefaultNodeTree 
{
	BBNode * root = [BBNode rootNode];
	root.title = @"Title for nice presentation";
	self.rootNode = root;
	
	BBNode * container = [BBNode containerNode];
	container.title = @"Magazine(s)";
	
	BBNode * collection = [BBNode collectionNode];
	
	BBNode * leaf1 = [BBNode leafNode];
	leaf1.title = @"one line title A";
	
	BBNode * leaf2 = [BBNode leafNode];
	leaf2.title = @"one line title B";
	
	[collection addChild:leaf1];
	[collection addChild:leaf2];
	[container addChild:collection];
	
	collection = [BBNode collectionNode];
	collection.isOpen = YES;
	
	leaf1 = [BBNode leafNode];
	leaf1.title = @"one line title C";
	
	[collection addChild:leaf1];
	[container addChild:collection];
	[container setPosition:NSMakePoint(10, 400)];
	
	collection = [BBNode collectionNode];
	
	leaf1 = [BBNode leafNode];
	leaf1.title = @"one line title D";
	
	leaf2 = [BBNode leafNode];
	leaf2.title = @"one line title E";
	
	[collection addChild:leaf1];
	[collection addChild:leaf2];
	[container addChild:collection];
	
	
	[self.rootNode addChild:container];
	
	BBNode * container2 = [BBNode containerNode];
	container2.title = @"Guides";
	[container2 setPosition:NSMakePoint(300, 400)];
	
	BBNode * collection2 = [BBNode collectionNode];
	collection2.title = @"Super Travel Guide";
	collection2.subtitle = @"Volume 23";
	
	BBNode * leaf1a = [BBNode leafNode];
	leaf1a.title = @"Prague";
	
	BBNode * leaf2a = [BBNode leafNode];
	leaf2a.title = @"Vienna";
	
	[collection2 addChild:leaf1a];
	[collection2 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"Paris";
	[collection2 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"Egar";
	[collection2 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"Barcelona";
	[collection2 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"London";
	[collection2 addChild:leaf2a];
	
	[container2 addChild:collection2];
	[self.rootNode addChild:container2];
	
	collection2 = [BBNode collectionNode];
	collection2.title = @"Super Travel";
	collection2.subtitle = @"Executive Summary";
	[container2 addChild:collection2];
	
	BBNode * container3 = [BBNode containerNode];
	container3.title = @"Online";
	[container3 setPosition:NSMakePoint(700, 400)];
	
	BBNode * collection3 = [BBNode collectionNode];
	collection3.title = @"Guidesonline.com";
	
	leaf1a = [BBNode leafNode];
	leaf1a.title = @"PragueOnline";
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"ViennaOnline";
	
	[collection3 addChild:leaf1a];
	[collection3 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"ParisOnline";
	[collection3 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"EgarOnline";
	[collection3 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"BarcelonaOnline";
	[collection3 addChild:leaf2a];
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"LondonOnline";
	[collection3 addChild:leaf2a];
	
	[container3 addChild:collection3];
	[self.rootNode addChild:container3];
	
	
	collection3 = [BBNode collectionNode];
	collection3.title = @"Zinesonline.com";
	
	leaf1a = [BBNode leafNode];
	leaf1a.title = @"PragueZineOnline";
	
	leaf2a = [BBNode leafNode];
	leaf2a.title = @"ViennaZineOnline";
	[collection3 addChild:leaf1a];
	[collection3 addChild:leaf2a];
	[container3 addChild:collection3];
	
	[self resetAllNodes];
	
	BBNode * containerZ = [BBNode containerNode];	
	BBNode * collectionZ = [BBNode collectionNode];
	BBNode * leafZ = [BBNode leafNode];
	[containerZ addChild:collectionZ];
	[collectionZ addChild:leafZ];
}


@end
